fail2ban — 入侵防御自动化封禁详解
fail2ban 是一个用 Python 编写的开源入侵防御工具(HIDS/IPS)。它扫描日志文件(如 /var/log/auth.log),一旦发现同一 IP 在短时间内出现多次认证失败,就通过修改系统防火墙规则(iptables / nftables / ipset 等)把该 IP 拒之门外,并在可配置的时间后自动解封。它开箱即可读懂 sshd、Apache、nginx 等几十种常见日志,也能轻易接入任何自研应用的日志。
本页先梳理 fail2ban 的"是什么"与行为/结构,再给出实战操作手册与参数指令速查。
1. 概述
1.1 是什么
fail2ban(f2b)由 Cyril Jaquier 于 2004 年编写,GPL v2 协议,当前由社区维护(v1.x,代码托管在 github.com/fail2ban/fail2ban)。它由三个可执行组件构成:
- fail2ban-server — 常驻后台的守护进程(daemon),真正干活:读日志、计数、封禁、解封。
- fail2ban-client — 控制客户端,通过 Unix socket 与 server 通信,负责查看状态、启停、手动封禁/解封、热重载配置等。官方强烈建议:永远通过
fail2ban-client操作,绝不要直接调用 fail2ban-server。 - fail2ban-regex — 正则测试工具,写 filter 前先用它验证 failregex 能否正确匹配日志。
核心数据结构是 Jail(监狱):每一个被保护的服务(如 sshd、apache)就是一个 jail,里面定义了"看哪个日志、怎么判定失败、失败多少次封禁、用什么方式封禁、封多久"。fail2ban 会为每个 jail 起一个独立线程持续运转。
1.2 核心心智模型:Jail = Filter + Action
记住一句话:Jail(监狱)= Filter(过滤器,负责"判罪")+ Action(动作,负责"执行刑罚")+ 判决参数(bantime / findtime / maxretry)。
# 一条典型日志行(sshd 认证失败) Feb 3 14:11:15 host sshd[25076]: Failed password for invalid user admin from 203.0.113.7 port 53526 ssh2 # 流程:datepattern 去掉时间 → failregex 匹配失败模式并抠出 IP # failregex 得到= 203.0.113.7 → 在 findtime(如10m) 内累计 ≥ maxretry(如5次) → 封禁 bantime(如1h)
四个"时间/次数"参数是所有 jail 的公共语言:
- findtime — "回看窗口":fail2ban 只统计最近 N 秒内发生的失败。
- maxretry — 窗口内失败次数达到该值就触发封禁。
- bantime — 封禁持续时长,到期自动解封。
- maxmatches — 每条封禁票据内存中保留的匹配日志行数。
1.3 生态与位置:IDS/IPS/防火墙
要理解 fail2ban 在安全体系中的位置,先分清这些角色:
- IDS(Intrusion Detection System,入侵检测系统) — 只"看"不"拦",发现异常报警。
- IPS(Intrusion Prevention System,入侵防御系统) — 检测到攻击后主动阻断。fail2ban 更接近 IPS,因为它真的会改防火墙规则封禁。
- HIDS(Host-based IDS/IPS) — 部署在单台主机上的检测/防御系统(相对 NIDS 部署在网络边界)。fail2ban 是典型的 HIDS。
- 防火墙(Firewall) — 内核级网络过滤(iptables/nftables),fail2ban 本身不负责过滤,而是指挥防火墙按需增减规则。
- backend(后端) — fail2ban 发现"日志有新内容"的机制:
pyinotify(内核 inotify 事件,最高效)、polling(轮询,无依赖)、systemd(直接读 systemd journal)。
- Jail(监狱) — fail2ban 中"一个受保护服务"的完整配置单元,含 filter、action、计数参数。
- Filter(过滤器) — 定义"如何从日志中判定失败并提取 IP"的正则集合(failregex/ignoreregex/prefregex)。
- Action(动作) — 定义"如何封禁/解封"的命令集合(actionban/actionunban/actionstart/actionstop/actioncheck)。
- Backend(后端) — 日志变更检测机制:pyinotify / polling / systemd。
- failregex(失败正则) — 匹配失败日志行的正则,必须能从匹配中提取
<HOST>。 - ignoreregex(忽略正则) — 即使命中 failregex 也忽略该行的正则(白名单日志行)。
- prefregex(预处理正则) — 在 failregex 之前统一提取公共部分或截取
<F-CONTENT>区段。 - datepattern(日期模式) — 匹配日志时间戳的模式,默认自动检测多种格式。
- Ticket(票据) — 一个 IP 在 failmanager 中的失败计数记录;升级为 BanTicket 后进入封禁管理。
- banaction(封禁动作名) — jail 引用的 action 文件名(默认 iptables-multiport)。
1.4 安装方式
| 发行版 | 安装命令 | 说明 |
|---|---|---|
| Debian / Ubuntu | sudo apt install fail2ban | 仓库版本通常较新 |
| RHEL / CentOS / Rocky | sudo yum install fail2ban | 可能需要 EPEL 源 |
| Fedora | sudo dnf install fail2ban | — |
| Arch / Manjaro | sudo pacman -S fail2ban | — |
| macOS (Homebrew) | brew install fail2ban | 配合 launchd 使用 |
环境要求:Python ≥ 3.5(可选依赖 pyinotify、python-systemd、dnspython)。源码安装:
git clone https://github.com/fail2ban/fail2ban.git cd fail2ban sudo python setup.py install # 自检与版本确认 fail2ban-client -h fail2ban-client version
启动与开机自启(systemd 发行版):
sudo systemctl enable --now fail2ban sudo systemctl status fail2ban
2. 行为图(系统"做什么")
2.1 用例图 — 交互全景
从外部视角看,谁在与 fail2ban 打交道:管理员通过 client 控制它;攻击者持续失败登录"喂"它;它则指挥防火墙、发通知。
2.2 活动图 — 从日志到封禁主流程
这是 fail2ban 的核心主流程:读日志 → 三层正则过滤 → 计数 → 到阈值 → 封禁 → 到期解封。
代码演示:三条 filter 规则如何层层把关
# filter.d/sshd.conf 中的判定链
[Definition]
prefailregex = ^<F-MLFID>%(__prefix_line)s<F-MLFID/>(?: %(__sshd_exclude)s)?(?: \\[pid <F-ID>\\d+<F-ID/>\\])? \
<F-CONTENT>.+</F-CONTENT>$
failregex = ^%(__prefix_line)s(?:error: )?Received disconnect from <HOST>.*: 3: .*: Auth fail$
^%(__prefix_line)sInvalid user <USER> from <HOST> port $
^%(__prefix_line)s(?:error: )?pam_unix\(sshd:auth\): authentication failure; logname=.* uid=.* euid=.* tty=.* ruser=.* rhost=<HOST>
ignoreregex =
2.3 时序图 — 攻击→封禁→解封
把一次完整的攻击生命周期画成时序:攻击者失败登录 → sshd 写日志 → fail2ban 读到并计数 → 触发 actionban → 防火墙拒绝 → bantime 到期 → actionunban 恢复。
2.4 状态机图 — 失败票据与封禁生命周期
一个 IP 在 fail2ban 内部的"案底"演化:从累积失败(Ticket)到被判刑(BanTicket),再到刑满释放(unban),或通过 recidive / increment 越封越重。
3. 结构图(系统"由什么组成")
3.1 部署图 — 生产部署拓扑
先看宏观部署:fail2ban 的 client/server 如何用 socket 通信,server 又连接了哪些外部资源(日志、防火墙、sqlite、SMTP、DNS)。
3.2 组件图 — 内部模块
再看进程内部:fail2ban 是 Python 源码,client 与 server 分离,server 内部按职责拆分为 filters、regex、failmanager、banmanager、actions、database 等模块。
3.3 包图 — /etc/fail2ban 配置布局
fail2ban 的"四个配置文件类型":fail2ban.conf(全局)、jail.conf(监狱)、filter.d/*.conf(过滤器)、action.d/*.conf(动作)。配置有严格的覆盖优先级。
*.conf 一律不修改(方便升级);所有自定义改动写进 *.local 或 .d/ 目录。同名的 .local 覆盖 .conf,晚解析的覆盖早解析的:jail.conf → jail.d/*.conf(字母序) → jail.local → jail.d/*.local(字母序)。
代码演示:只改一个文件启用默认 sshd 监狱
# 创建 /etc/fail2ban/jail.local,仅写想改的项即可 [sshd] enabled = true maxretry = 5 findtime = 10m bantime = 1h
3.4 类图 — 核心类结构
最后细化到核心类:JailThread 是每个监狱的运转线程,持有 Filter、FailManager、BanManager、Actions;通过 Transmitter 接收 client 命令。
4. 实战操作手册
4.1 安装与启动
# Debian/Ubuntu sudo apt update && sudo apt install -y fail2ban # 启动并开机自启 sudo systemctl enable --now fail2ban # 确认进程与版本 systemctl status fail2ban fail2ban-client version # v1.x fail2ban-client ping # 返回 pong 说明 server 存活
4.2 基础配置:启用 sshd 监狱
永远不要直接编辑 /etc/fail2ban/jail.conf(升级会被覆盖)。新建 jail.local:
# /etc/fail2ban/jail.local [DEFAULT] # 全局默认:10分钟内失败5次,封禁1小时 bantime = 1h findtime = 10m maxretry = 5 # 永不封禁自己的/内网/办公出口 IP(用逗号分隔,支持 CIDR) ignoreip = 127.0.0.1/8 ::1 192.168.0.0/16 10.0.0.0/8 # 默认封禁动作:按多端口写 iptables 规则(IPv4+IPv6 自动) banaction = iptables-multiport # 有 SMTP 的话可加邮件通知 # action = %(action_mwl)s # 会同时调用 sendmail 发通知 [sshd] enabled = true
应用配置并验证:
sudo systemctl reload fail2ban # 热重载(不重启 server) fail2ban-client status # 列出所有监狱 fail2ban-client status sshd # 查看 sshd 监狱详情
reload 会按新配置重建受影响的监狱,但不会清空已有封禁。想彻底重启监狱并清空封禁用 fail2ban-client reload --restart --unban sshd。
4.3 自定义 Filter(新应用接入)
以监控自研应用(如后端 API 的登录接口)为例:日志形如 2026-08-20 10:00:01 ERROR login failed ip=203.0.113.9 user=admin。
# 1) 新建过滤器 /etc/fail2ban/filter.d/myapp.conf [Definition] # 匹配失败日志并抠出 IP;是内置的"IP或主机名"正则 failregex = ^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} ERROR login failed ip=<HOST> ignoreregex = ^.*user=(?:test|root)$ # 测试账号失败不封禁 # 2) 新建监狱 /etc/fail2ban/jail.local 追加 [myapp] enabled = true filter = myapp logpath = /var/log/myapp/api.log backend = polling maxretry = 3 findtime = 5m bantime = 10m
先用 fail2ban-regex 验证正则(见 4.7),再 reload:
sudo systemctl reload fail2ban fail2ban-client status myapp
4.4 自定义 Action(封禁+通知)
默认的 iptables-multiport 只做"DROP",可以再加动作:邮件通知、调 webhook、或自定义命令。
# /etc/fail2ban/action.d/mywebhook.local
[Definition]
actionban = curl -s -X POST http://alert.example.com/ban \
-d 'ip=<ip>&count=<bancount>&jail=<name>'
actionunban = curl -s -X POST http://alert.example.com/unban \
-d 'ip=<ip>&jail=<name>'
actionstart = echo "jail <name> started at <time>"
actionstop = echo "jail <name> stopped at <time>"
# jail.local 里给 sshd 同时挂默认防火墙动作 + 通知动作(可挂多个)
[sshd]
enabled = true
action = iptables-multiport[name=sshd, port="ssh", protocol=tcp]
mywebhook
动作命令通过 shell 执行,<ip>、<name>、<time>、<failures>、<matches>、<bancount> 等标签会被自动替换(完整标签见 5.4)。
4.5 日常运维命令
fail2ban-client status # 全部监狱状态 fail2ban-client status sshd # 单个监狱:封禁列表/失败统计 fail2ban-client status --all # 详细模式 fail2ban-client stats # 全部监狱统计表格 fail2ban-client set sshd bantime 2h # 热修改封禁时长 fail2ban-client set sshd banip 203.0.113.9 # 手动立即封禁一个 IP fail2ban-client set sshd unbanip 203.0.113.9 # 手动解封 fail2ban-client unban --all # 清空全部封禁 fail2ban-client get sshd banned # 列出该监狱封禁的 IP fail2ban-client reload # 热重载配置 fail2ban-client restart # 重启整个 server fail2ban-client set loglevel DEBUG # 临时调日志级别调试
4.6 进阶:渐进封禁与 recidive
对付顽固攻击者:bantime.increment 让封禁时长逐次翻倍;recidive 监狱专门"封禁那些反复被其他监狱封禁的 IP"。
# jail.local 开启渐进封禁(全部监狱生效) [DEFAULT] bantime.increment = true bantime.factor = 2 # 每次翻倍 bantime.maxtime = 1w # 封禁时长上限 bantime.random = true # 加入随机扰动防规律 bantime.overalljails = true # 跨监狱累计次数 # recidive 监狱:24h 内被其它监狱封禁 ≥3 次的 IP,直接封 1 周 [recidive] enabled = true logpath = /var/log/fail2ban.log banaction = iptables-allports bantime = 1w findtime = 1d maxretry = 3
4.7 正则测试:fail2ban-regex
写正则最容易出错,务必先用工具验证再上线:
# 用法: fail2ban-regex <LOG> <REGEX> [IGNOREREGEX] # LOG 可以是:日志文件路径 / systemd-journal / 单行字符串 fail2ban-regex /var/log/auth.log '^.*Failed password for invalid user admin from <HOST> port ' fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf fail2ban-regex '2026-08-20 10:00:01 ERROR login failed ip=203.0.113.9' \ '^.*login failed ip=<HOST> ' # 常用选项 # -r/--raw 不解析 DNS,只看 IP # --usedns=no 只匹配 IP 不匹配主机名 # -d PATTERN 自定义 datepattern # -L N 多行正则的 maxlines # -o 'ip,msg' 只输出 ip 和 msg 字段 fail2ban-regex --usedns=no -r /var/log/auth.log sshd
输出会告诉你:Successfully matched(成功匹配数)与 Date / IP / Host 命中统计。只要 Successfully matched > 0 且 IP 列正确,这个 filter 才能用。
5. 参数指令速查手册
5.1 fail2ban-client 命令一览
| 分类 | 命令 | 说明 |
|---|---|---|
| 基础 | start | 启动 server 和所有监狱 |
restart | 重启 server | |
reload [--restart] [--unban] [--if-exists] [<jail>] | 热重载配置;--restart 重启受影响监狱;--unban 同时清空封禁 | |
stop | 停止所有监狱并终止 server | |
| 查询 | status [<jail>] [FLAVOR] | server 或某监狱状态(basic/cymru/short/stats) |
statistics | 全部监狱统计表 | |
banned [<ip>...] | 有封禁的监狱字典 / 指定 IP 在哪些监狱被封 | |
ping / version | 探活 / 版本 | |
| 封禁 | unban --all | 清空所有封禁(含数据库) |
unban <ip>... | 解封指定 IP(所有监狱+数据库) | |
| 日志 | set loglevel <LEVEL> | 设置级别 CRITICAL/ERROR/WARNING/NOTICE/INFO/DEBUG/TRACEDEBUG/HEAVYDEBUG |
set logtarget <TARGET> | STDOUT/STDERR/SYSLOG/SYSTEMD-JOURNAL/文件 | |
flushlogs | 关闭并重开日志文件(配合 logrotate) | |
| 数据库 | set dbfile <FILE> | 持久化库路径(None 关闭) |
set dbmaxmatches <INT> | 每票据数据库保存的最大匹配行数(默认10) | |
set dbpurgeage <SECONDS> | 封禁历史保留时长(默认86400) | |
| 监狱控制 | add <jail> <backend> | 创建监狱(指定 backend) |
start <jail> / stop <jail> | 启动/停止并移除某监狱 | |
status <jail> | 某监狱详情 | |
| 监狱配置 | set <jail> banip <ip>... | 手动封禁 |
set <jail> unbanip <ip>... | 手动解封 | |
set <jail> bantime <TIME> | 改封禁时长 | |
set <jail> maxretry <RETRY> | 改失败阈值 | |
set <jail> findtime <TIME> | 改回看窗口 | |
set <jail> addignoreip <ip> / delignoreip | 增删白名单 |
5.2 jail.conf 参数表
| 参数 | 默认 | 说明 |
|---|---|---|
enabled | false | 是否启用该监狱 |
filter | — | 使用的过滤器名(filter.d 下的文件名去扩展名) |
logpath | — | 监控的日志文件,支持通配符 glob,可加 tail 选项(从尾部读) |
backend | auto | auto / pyinotify / polling / systemd |
bantime | 10m | 封禁时长 |
findtime | 10m | 失败统计回看窗口 |
maxretry | 5 | 窗口内失败次数阈值 |
ignoreip | 127.0.0.1/8 ::1 | 白名单,支持 CIDR、DNS、file: 前缀加载外部名单 |
ignoreself | true | 不封禁本机自身 IP |
ignorecommand | — | 封禁前执行的判断命令,退出码0则不封禁;可用 <ip> 等标签 |
ignorecache | — | ignore 判断的缓存(key/max-count/max-time) |
banaction | iptables-multiport | 默认封禁动作名 |
banaction_allports | iptables-allports | allports 类监狱(pam-generic/recidive)用 |
action | %(action_mw)s | 动作列表,可带参数 iptables-ipset[type=multiport, blocktype=DROP],可配多个 |
maxmatches | 同 maxretry | 每票据内存保留的匹配行数 |
usedns | warn | 是否解析日志中的主机名:yes/no/warn |
logencoding | auto | 日志文件编码 |
logtimezone | — | 无时区日志的强制时区(Z/UTC±hhmm/GMT±hhmm) |
skip_if_nologs | false | 匹配不到日志文件时跳过该监狱而非报错 |
systemd_if_nologs | true | auto 后端找不到文件时可切到 systemd journal |
bantime.increment | false | 渐进封禁(每次翻倍) |
bantime.factor / maxtime / random / overalljails | — | 渐进封禁的系数/上限/随机/跨监狱累计 |
5.3 filter.d 参数表
| 参数 | 说明 |
|---|---|
prefregex | 公共部分预处理正则;可用 <F-CONTENT>... 截取内容、<F-MLFID> 关联多行 |
failregex | 判定失败的核心正则,必须能提取 <HOST> 或 <F-ID> |
ignoreregex | 匹配即忽略(即使命中 failregex) |
maxlines | 多行正则的缓冲行数 |
datepattern | 自定义时间匹配,支持 Epoch/TAI64N/ISO8601,{DEFAULT}/{^LN-BEG} 等 |
journalmatch | systemd journal 的匹配表达式(journalctl 语法) |
正则内建标签:
| 标签 | 含义 |
|---|---|
<HOST> | IP 或主机名(受 usedns 影响) |
<ADDR> | IPv4/IPv6 地址 |
<IP4> / <IP6> | IPv4 / IPv6 |
<DNS> | 主机名 |
<CIDR> / <SUBNET> | CIDR 掩码 / IP/CIDR 网段 |
<F-ID>...<F-ID> | 不用 IP 时的自定义封禁标识(如 user id) |
<F-USER>...</F-USER> | 自由命名捕获组,可在 action 中用 <F-USER> 引用 |
<SKIPLINES> | 多行正则中允许跳过的行 |
5.4 action.d 参数表与标签
| 参数 | 触发时机 |
|---|---|
actionstart | 监狱启动时(如创建防火墙链) |
actionstop | 监狱停止时(如删除链) |
actioncheck | 每次封禁/解封前检查环境;退出码非0会触发重初始化(先 stop 再 start) |
actionban | 达到 maxretry 触发封禁时执行 |
actionunban | bantime 到期或手动解封时执行 |
timeout | 命令最大执行时长(秒),超时被杀 |
命令中可替换的标签(<ip>、<time> 等):
| 标签 | 含义 |
|---|---|
<ip> | 被封 IP |
<family> | inet4 / inet6 |
<failures> | 本次票据的失败次数 |
<time> | 封禁的 Unix 时间戳 |
<bantime> | 本次实际封禁时长(秒) |
<bancount> | 该 IP 累计被封次数 |
<matches> | 触发封禁的日志行拼接字符串(已转义防注入) |
<ip-rev> / <ip-host> | IP 的 PTR 记录 / 反解主机名 |
<fid> | 封禁事件标识(Ticket 标识符) |
<restored> | 1=从数据库恢复的封禁,0=新封禁 |
<F-*> | filter 中 <F-TAG> 捕获的自由字段 |
<ipfailures> / <ipjailfailures> | 基于数据库的跨监狱 / 本监狱累计失败数 |
<jail.banned> / <jail.found> | 该监狱当前封禁数 / 当前失败票据数 |
5.5 时间缩写格式
bantime、findtime 等时间参数既可用整数秒,也可用缩写:
| 单位 | 缩写 | 示例 |
|---|---|---|
| 年 | years / yea / yy | 1y = 31536000s |
| 月 | months / mon | 1mo |
| 周 | weeks / wee / ww | 1w |
| 天 | days / da / dd | 1d |
| 小时 | hours / hou / hh | 12h |
| 分钟 | minutes / min / mm | 10m |
| 秒 | seconds / sec / ss | 30s |
可组合:1d12h30m;注意 m/mm 是分钟,月份要用 mo/mon。换算验证:
fail2ban-client --str2sec 1d12h # 输出 129600
6. 故障排查与常见问题
| 现象 | 原因 | 排查 / 解决 |
|---|---|---|
启 jail 报 Have not found any log file | logpath 通配符没匹配到文件 | 确认路径与权限;可加 skip_if_nologs = true;或允许 auto 后端切 systemd |
| 该封的 IP 没被封 | failregex 没匹配 / 命中 ignoreip / 反解 DNS 对不上 | 用 fail2ban-regex 实测;查看 /var/log/fail2ban.log 的 DEBUG 输出 |
日志里 RepeatedMsgReduction 导致漏检 | rsyslog 把重复消息压缩成 "last message repeated N times" | 在 rsyslog 配置中关闭 RepeatedMsgReduction |
| 封禁不生效(防火墙没规则) | action 名称/参数错误,或 iptables 权限不足 | fail2ban-client get sshd actions 查看已挂动作;手跑 actionban 看报错 |
| 误封了正常 IP | 回看窗口太短 / maxretry 太低 / 反解 DNS 不对称 | 加大 findtime、调高 maxretry;把 usedns 设 no(只认 IP);加 ignoreip |
| IPv6 不生效 | allowipv6 设置或后端不支持 | 确认 allowipv6;确认 iptables6/nft 规则链 |
| 重启后封禁丢失 | dbfile 未启用或损坏 | 确认 /var/lib/fail2ban/fail2ban.sqlite3 存在且可写 |
| 日志文件轮转后读不到 | logrotate 与 fail2ban 未联动 | 用 flushlogs 重开日志;或按发行版配置 logrotate 的 postrotate 脚本 |
fail2ban-client ping 失败 | server 未运行 / socket 路径不对 | systemctl status fail2ban;确认 /var/run/fail2ban/fail2ban.sock 存在 |
7. 参考资料
- 官方站点:https://www.fail2ban.org
- 源码仓库:https://github.com/fail2ban/fail2ban
- Wiki / FAQ / HOWTO:https://github.com/fail2ban/fail2ban/wiki
- 开发者文档:https://fail2ban.readthedocs.io/
- man 手册:
fail2ban-client(1)、fail2ban-server(1)、fail2ban-regex(1)、fail2ban.conf(5)、jail.conf(5)